/*
 * Created on 2006-11-8
 *
 */
package jacky.lanlan.song.extension.spring;

import jacky.lanlan.song.util.Logger;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

/**
 * <p>
 * Spring 加载器，Singleton。
 * <p>
 * 负责从Spring中读取对应名字的Bean的实用类。
 * <p>
 * BeanHome贯彻IoC思想，应用程序应该从某个地方(比如一个自动 启动的Servlet，或者某个"Bean")获得一个Spring的
 * ApplicationContext，然后“注入”到BeanHome中。
 * <p>
 * BeanHome将把注入的ApplicationContext变成Singleton，供以后使用。
 * <p>
 * 除了注入一个ApplicationContext外，BeanHome还可以从指定的XML配置文件
 * 创建一个ApplicationContext，并保证该ApplicationContext是一个Singleton。 <p
 * style="color:red;font-weight:bolder;font-size:13px;">
 * BeanHome将遵循以下步骤启动Spring并提供ApplicationContext： <ol style="color:red;font-size:13px;">
 * <li>
 * 若有一个立即可用的ApplicationContext被注入，则BeanHome优先使用它。</li>
 * <li>
 * 若1失败，BeanHome将根据 setConfigFilePath_ClassPath()方法提供的配置文件路径，读取 配置文件并启动Spring。</li>
 * <li>
 * 若2失败，BeanHome将根据 setConfigFilePath_FilePath()方法提供的配置文件路径，读取 配置文件并启动Spring。</li>
 * <li>
 * 若3失败，BeanHome将在根类路径下寻找默认配置文件 applicationContext.xml并试图用它来启动Spring。</li>
 * <li>
 * 若以上步骤都失败，则当调用getContext()方法时将抛出RuntimeException。</li> </ol>
 * 
 * @author Jacky.Song
 */
public class BeanHome {
	
	private final static Logger logger = Logger.getLogger();
	
	private static ApplicationContext ctx=null;
	private static String[] configFile_Classpath=null;
	private static String[] configFile_Filepath=null;
	private static final Lock lock=new ReentrantLock();
	
	private BeanHome() {}
	
	/**
	 * 根据Bean id 获取Spring管理的Bean。
	 * 
	 * @param id
	 *          注册到Spring中的Bean的id
	 * @return 对应的Bean实例
	 */
	public static Object getSpringBean(String id) {
		return getContext().getBean(id);
	}
	
	/**
	 * 根据Bean 类型获取Spring管理的Bean。 <p style="color:red"> 该方法假设Bean的 id 为beanType的简单类名。即：
	 * <code>beanType.getSimpleName()</code>
	 * 
	 * @param beanType
	 *          Bean类型，父类或接口类型
	 * @return 对应的Bean实例
	 * @see #getSpringBean(String, Class)
	 */
	public static <T> T getSpringBean(Class<T> beanType) {
		return beanType.cast(
						getContext().getBean(beanType.getSimpleName(), beanType));
	}
	
	/**
	 * 根据Bean id和类型获取Spring管理的Bean。
	 * 
	 * @param id
	 *          注册到Spring中的Bean的id
	 * @param beanType
	 *          Bean类型，父类或接口类型
	 * @return 对应的Bean实例
	 */
	public static <T> T getSpringBean(String id,Class<T> beanType) {
		return beanType.cast(
				getContext().getBean(id, beanType));
	}
	
	/**
	 * 查询指定名称的Bean是否是<code>Prototype</code>。
	 * 
	 * @param name
	 *          Bean 名称
	 * @return true如果是，false如果不是
	 * @see org.springframework.beans.factory.BeanFactory.isPrototype()
	 */
	public static boolean isPrototype(String name) {
		return getContext().isPrototype(name);
	}
	
	/**
	 * 查询指定名称的Bean是否是<code>Singleton</code>。
	 * 
	 * @param name
	 *          Bean 名称
	 * @return true如果是，false如果不是
	 * @see org.springframework.beans.factory.BeanFactory.isSingleton()
	 */
	public static boolean isSingleton(String name) {
		return getContext().isSingleton(name);
	}
	
	/**
	 * “注入”Spring的ApplicationContext。
	 * <p>
	 * 若该方法和setConfigFilePath_ClassPath()、setConfigFilePath_FilePath()
	 * 方法都被调用，BeanHome将优先使用该方法提供的ApplicationContext。
	 * 
	 * @param ctx
	 *          Spring的ApplicationContext
	 */
	public static void setApplicationContext(ApplicationContext ctx) {
		BeanHome.ctx = ctx;
	}
	
	/**
	 * 设置Spring的配置文件路径。当没有找到注入的ApplicationContext时，BeanHome会 使用这里提供的配置文件重新创建一个ApplicationContext。
	 * <p>
	 * <em style="font-size:15px;color:red"> 注意，配置文件必须处于classpath下。<br/>
	 * 若该方法与setConfigFilePath_FilePath()方法都被调用，BeanHome将优先使用 这里提供的路径。 </em>
	 * 
	 * @param path
	 *          Classpath下的Spring配置文件地址
	 */
	public static void setConfigFilePath_ClassPath(String [] path) {
		configFile_Classpath=path;
	}
	
	/**
	 * 设置Spring的配置文件路径。当没有找到注入的ApplicationContext时，BeanHome会 使用这里提供的配置文件重新创建一个ApplicationContext。
	 * 
	 * @param path
	 *          文件系统下的Spring配置文件地址
	 */
	public static void setConfigFilePath_FilePath(String [] path) {
		configFile_Filepath=path;
	}
	
	/*
	 * 根据提供的配置文件，创建一个ApplicationContext。
	 */
	private static ApplicationContext createSingleton() {
		ApplicationContext context=null;
		lock.lock();
		try {
			context = createApplicationContext();
		}
		finally {
			lock.unlock();
		}
		return context;
	}
	
	// 创建ApplicationContext
	private static ApplicationContext createApplicationContext() {
		if (ctx == null) {// 第二次检测，注意这次是在同步方法中，所以可以避免线程争用
			logger.debug("没有立即可用的ApplicationContext，尝试从类路径装载配置文件...");

			// 执行第2步，检测类路径下的配置文件
			if(configFile_Classpath!=null) {
				try {
					ctx = new ClassPathXmlApplicationContext(configFile_Classpath);
					logger.debug("从 类路径配置文件 启动Spring成功");
					return ctx;
				}
				catch (Exception e) {
					logger.error(e);
					logger.error("使用 类路径配置文件 启动Spring出错，尝试从文件系统装载配置文件...");
				}
			}
			else {
				logger.debug("没有设置 类路径配置文件 ，尝试从文件系统路径装载配置文件");
			}
			
			// 执行第3步，检测文件系统下的配置文件
			if(configFile_Filepath!=null) {
				try {
					ctx = new FileSystemXmlApplicationContext(configFile_Filepath);
					logger.debug("从 文件系统路径配置文件 启动Spring成功");
					return ctx;
				}
				catch (Exception e) {
					logger.error(e);
					logger.error("使用 文件系统路径配置文件 启动Spring出错，尝试读取默认配置文件...");
				}
			}
			else {
				logger.debug("没有设置 文件系统路径配置文件 ，尝试读取默认配置文件(applicationContext.xml)");
			}
			
			// 执行第4步，检测默认配置文件
			try {
				ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
				logger.debug("从 默认配置文件(applicationContext.xml) 启动Spring成功");
				return ctx;
			}
			catch (Exception e) {
				logger.error(e);
				logger.error("使用 默认配置文件(applicationContext.xml) 启动Spring出错，停止启动 Spring。");
			}
			
			// 以上都失败，抛出异常
			throw new RuntimeException(
					"Fail to start Spring.Check the config file path or inject an ApplicationContext"
			);
		}
		return ctx;
	}
	
	/**
	 * 获取Spring的ApplicationContext。<br>
	 * 这个方法保证返回一个可用的ApplicationContext，永不返回null。
	 * <p>
	 * <em style="font-size:15px;color:red"> 注意：当BeanHome无法返回ApplicationContext时，调用这个方法
	 * 将抛出RuntimeException。 </em>
	 * 
	 * @return 立即可用的Spring ApplicationContext
	 */
	public static ApplicationContext getContext() {
		//Lazy load + Double-Checked Loking
		return (ctx==null) ? createSingleton() : ctx;
	}

}
